/****************************************************************************
 * Copyright (c) 2010 Torkild U. Resheim
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Torkild U. Resheim  - initial API and implementation
 *****************************************************************************/
package no.resheim.buildmonitor.core;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.HashMap;
import java.util.Observable;

import no.resheim.buildmonitor.core.data.IChangeReporter;
import no.resheim.buildmonitor.core.data.Server;

import org.eclipse.core.resources.ISaveContext;
import org.eclipse.core.resources.ISaveParticipant;
import org.eclipse.core.resources.ISavedState;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Plugin;
import org.eclipse.core.runtime.Status;
import org.eclipse.equinox.security.storage.ISecurePreferences;
import org.eclipse.equinox.security.storage.SecurePreferencesFactory;
import org.eclipse.equinox.security.storage.StorageException;
import org.osgi.framework.BundleContext;

/**
 * Keeps track of all build servers and persists these to the file system when
 * asked to do so by the workspace save mechanism.
 * 
 * @author Torkild Ulvøy Resheim
 * @since 1.0
 */
public class BuildmonitorCore extends Plugin {

	public static class ServerList extends Observable implements
			IChangeReporter {
		/**
		 * The list of servers.
		 */
		private final HashMap<URI, Server> servers;

		private ServerList() {
			servers = new HashMap<URI, Server>();
		}

		/**
		 * Returns the list of servers.
		 * 
		 * @return the list of servers.
		 */
		public Collection<Server> getServers() {
			return servers.values();
		}

		private String changeDescription;

		/**
		 * Returns a description or summary of the last change that occurred in
		 * the server.
		 * 
		 * @return a description of the change
		 */
		@Override
		public String getChangeDescription() {
			return changeDescription;
		}

		/**
		 * Sets the description of the last change.
		 * 
		 * @param changeDescription
		 *            a short description
		 */
		@Override
		public void setChangeDescription(String changeDescription) {
			this.changeDescription = changeDescription;
		}

		/**
		 * Adds a new build server to the list of servers to maintain.
		 * 
		 * @param server
		 *            the server to build
		 */
		public void addServer(Server server, String message) {
			servers.put(server.getUri(), server);
			setChangeDescription(message);
			setChanged();
			notifyObservers(this);
		}

		public void removeServer(URI uri, String message) {
			Server server = servers.remove(uri);
			if (server != null) {
				server.disconnect();
				setChangeDescription(message);
				setChanged();
				notifyObservers(this);
			}
		}

		public Server getServer(URI uri) {
			return servers.get(uri);
		}

		/**
		 * Checks if a server with the given URI is present in the list and
		 * returns <code>true</code> if this is the case.
		 * 
		 * @param uri
		 *            the server URI
		 * @return <code>true</code> if the server is present
		 */
		public boolean hasServer(URI uri) {
			return (servers.containsKey(uri));
		}

		/**
		 * Removes the build server from the system and allows it to disconnect.
		 * 
		 * @param server
		 *            the server to remove
		 */
		public void removeServer(Server server, String message) {
			removeServer(server.getUri(), message);
		}
	}

	/**
	 * Stores the list of build server URIs along with the server {@link Class}
	 * when asked to do so by the workbench.
	 * 
	 * @author Torkild Ulvøy Resheim
	 * @since 1.0
	 */
	private class WorkspaceSaveParticipant implements ISaveParticipant {

		@Override
		public void doneSaving(ISaveContext context) {
			// delete the old saved state since it is not necessary anymore
			int previousSaveNumber = context.getPreviousSaveNumber();
			String oldFileName = "save-" + Integer.toString(previousSaveNumber); //$NON-NLS-1$
			File f = getStateLocation().append(oldFileName).toFile();
			if (!f.delete()) {
				getLog().log(
						new Status(IStatus.ERROR, PLUGIN_ID, MessageFormat
								.format(Messages.BuildmonitorCore_1,
										new Object[] { f })));
			}
		}

		/**
		 * The persisted server list is a text file with one server instance
		 * represented on each line. The line contains two items; the server
		 * implementation class and the URI separated by a pipe character (|).
		 * 
		 * @param file
		 *            the file to write
		 * @throws IOException
		 */
		private void doSave(File file) throws IOException {
			FileWriter fw = new FileWriter(file);
			for (Server server : list.getServers()) {
				fw.write(server.getClass().getName());
				fw.write('|');
				fw.write(server.getUri().toString());
				fw.write('\n');
			}
			fw.close();
		}

		@Override
		public void prepareToSave(ISaveContext context) throws CoreException {
		}

		@Override
		public void rollback(ISaveContext context) {
			// since the save operation has failed, delete the saved state we
			// have just written
			int saveNumber = context.getSaveNumber();
			String saveFileName = "save-" + Integer.toString(saveNumber); //$NON-NLS-1$
			File f = getStateLocation().append(saveFileName).toFile();
			if (!f.delete()) {
				getLog().log(
						new Status(IStatus.ERROR, PLUGIN_ID, MessageFormat
								.format(Messages.BuildmonitorCore_3,
										new Object[] { f })));
			}
		}

		@Override
		public void saving(ISaveContext context) throws CoreException {
			// save the plug-in state
			int saveNumber = context.getSaveNumber();
			String saveFileName = "save-" + Integer.toString(saveNumber); //$NON-NLS-1$
			File f = getStateLocation().append(saveFileName).toFile();
			// if we fail to write, an exception is thrown and we do not update
			// the path
			try {
				doSave(f);
				context.map(new Path("save"), new Path(saveFileName)); //$NON-NLS-1$
				context.needSaveNumber();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	private static BuildmonitorCore plugin;

	public static final String PLUGIN_ID = Messages.BuildmonitorCore_6;

	/** Preference key for the secure storage password */
	public static final String SECURE_STORAGE_PASSWORD = "password"; //$NON-NLS-1$

	/** Preference key for the secure storage root */
	public static final String SECURE_STORAGE_ROOT = PLUGIN_ID;

	/** Preference key for the secure storage user name */
	public static final String SECURE_STORAGE_USERNAME = "username"; //$NON-NLS-1$

	public static BuildmonitorCore getDefault() {
		return plugin;
	}

	/**
	 * List of factories to use when populating the servers list as the
	 * workbench is starting.
	 */
	private final HashMap<String, IServerFactory> factories;

	private final ServerList list;

	public Collection<IServerFactory> getServerFactories() {
		return factories.values();
	}

	public BuildmonitorCore() {
		factories = new HashMap<String, IServerFactory>();
		list = new ServerList();
	}

	public void storeCredentials(URI uri, String login, String password)
			throws StorageException {
		ISecurePreferences root = SecurePreferencesFactory.getDefault().node(
				SECURE_STORAGE_ROOT);
		ISecurePreferences serverNode = root.node(uri.getHost());
		serverNode.put(SECURE_STORAGE_USERNAME, login, false);
		serverNode.put(SECURE_STORAGE_PASSWORD, password, true);
	}

	public String getPassword(URI uri) throws StorageException {
		ISecurePreferences root = SecurePreferencesFactory.getDefault().node(
				SECURE_STORAGE_ROOT);
		ISecurePreferences serverNode = root.node(uri.getHost());
		return serverNode.get(SECURE_STORAGE_PASSWORD, "");
	}

	public String getUsername(URI uri) throws StorageException {
		ISecurePreferences root = SecurePreferencesFactory.getDefault().node(
				SECURE_STORAGE_ROOT);
		ISecurePreferences serverNode = root.node(uri.getHost());
		return serverNode.get(SECURE_STORAGE_USERNAME, "");
	}

	/**
	 * Iterates through extension declarations and creates instances of
	 * {@link IServerFactory} to use when populating the servers list as the
	 * workbench starts.
	 */
	private void loadFactories() {
		final IExtensionRegistry ereg = Platform.getExtensionRegistry();
		IConfigurationElement[] elements = ereg
				.getConfigurationElementsFor(Messages.BuildmonitorCore_7);
		for (IConfigurationElement configurationElement : elements) {
			if (configurationElement.getName().equals("factory")) { //$NON-NLS-1$
				String serverClass = configurationElement
						.getAttribute("server"); //$NON-NLS-1$
				String factoryClass = configurationElement
						.getAttribute("class"); //$NON-NLS-1$
				try {
					Object object = configurationElement
							.createExecutableExtension("class"); //$NON-NLS-1$
					if (object instanceof IServerFactory) {
						factories.put(serverClass, (IServerFactory) object);
					} else {
						getLog().log(
								new Status(IStatus.ERROR, PLUGIN_ID,
										MessageFormat.format(
												Messages.BuildmonitorCore_12,
												new Object[] { serverClass,
														factoryClass })));
					}
				} catch (CoreException e) {
					getLog().log(
							new Status(IStatus.ERROR, PLUGIN_ID, MessageFormat
									.format(Messages.BuildmonitorCore_13,
											new Object[] { factoryClass }), e));
				}
			}
		}
	}

	/**
	 * Loads the list of server URIs and associated {@link Server} class names.
	 * For each item the server factory will be used to create the
	 * {@link Server} instance and connect to the URI.
	 * 
	 * @param file
	 *            the file to load from
	 * @throws IOException
	 */
	private void loadServers(File file) throws IOException {
		BufferedReader fr = new BufferedReader(new FileReader(file));
		String in = null;
		while ((in = fr.readLine()) != null) {
			String[] items = in.split("\\|"); //$NON-NLS-1$
			try {
				Server s = (factories.get(items[0])).createInstance(new URI(
						items[1]));
				list.addServer(s, null);
				s.connect();
			} catch (URISyntaxException e) {
				e.printStackTrace();
			}
		}
		fr.close();
	}

	public ServerList getServers() {
		return list;
	}

	@Override
	public void start(BundleContext context) throws Exception {
		super.start(context);
		plugin = this;
		loadFactories();
		ISaveParticipant saveParticipant = new WorkspaceSaveParticipant();
		ISavedState lastState = ResourcesPlugin.getWorkspace()
				.addSaveParticipant(PLUGIN_ID, saveParticipant);
		if (lastState == null)
			return;
		IPath location = lastState.lookup(new Path("save")); //$NON-NLS-1$
		if (location == null)
			return;
		File f = getStateLocation().append(location).toFile();
		loadServers(f);
	}

	@Override
	public void stop(BundleContext context) throws Exception {
		plugin = null;
		super.stop(context);
		for (Server server : list.getServers()) {
			server.disconnect();
		}
	}

}
